package com.zm.xiaohashu.note.biz.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.alibaba.nacos.shaded.com.google.common.base.Preconditions;
import com.alibaba.nacos.shaded.com.google.common.collect.Lists;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.zm.exception.BizException;
import com.zm.response.Response;
import com.zm.utils.DateUtils;
import com.zm.xiaohashu.biz.context.utils.LoginUserContextHolder;
import com.zm.xiaohashu.model.req.DeleteNoteReqVO;
import com.zm.xiaohashu.model.req.FindNoteDetailReqVO;
import com.zm.xiaohashu.model.req.PublishNoteReqVO;
import com.zm.xiaohashu.model.req.UpdateNoteReqVO;
import com.zm.xiaohashu.model.resp.FindNoteDetailRspVO;
import com.zm.xiaohashu.note.biz.constant.MQConstants;
import com.zm.xiaohashu.note.biz.constant.RedisConstant;
import com.zm.xiaohashu.note.biz.constant.RedisKeyConstants;
import com.zm.xiaohashu.note.biz.domain.Note;
import com.zm.xiaohashu.note.biz.domain.NoteLike;
import com.zm.xiaohashu.note.biz.domain.Topic;
import com.zm.xiaohashu.note.biz.enums.*;
import com.zm.xiaohashu.note.biz.mapper.NoteLikeMapper;
import com.zm.xiaohashu.note.biz.mapper.NoteMapper;
import com.zm.xiaohashu.note.biz.mapper.TopicMapper;
import com.zm.xiaohashu.note.biz.model.dto.LikeUnlikeNoteMqDTO;
import com.zm.xiaohashu.note.biz.model.vo.LikeNoteReqVO;
import com.zm.xiaohashu.note.biz.model.vo.UnlikeNoteReqVO;
import com.zm.xiaohashu.note.biz.rpc.DistributedIdGeneratorRpcService;
import com.zm.xiaohashu.note.biz.rpc.KeyValueRpcService;
import com.zm.xiaohashu.note.biz.rpc.UserRpcService;
import com.zm.xiaohashu.note.biz.service.NoteService;
import com.zm.xiaohashu.user.model.vo.FindUserByIdRspDTO;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.client.producer.SendCallback;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.scripting.support.ResourceScriptSource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionTemplate;

import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.TimeUnit;

import static com.zm.xiaohashu.note.biz.enums.ResponseCodeEnum.NOTE_NOT_LIKED;


/**
 * @author 24690
 * @description 针对表【t_note(笔记表)】的数据库操作Service实现
 * @createDate 2025-08-14 13:11:51
 */
@Service
@Slf4j
public class NoteServiceImpl extends ServiceImpl<NoteMapper, Note> implements NoteService {

    @Resource
    private TopicMapper topicMapper;
    @Resource
    private NoteMapper noteMapper;
    @Resource
    private KeyValueRpcService keyValueRpcService;
    @Resource
    private DistributedIdGeneratorRpcService idGeneratorRpcService;
    @Resource
    private RedisTemplate<String, Object> redisTemplate;
    @Resource
    private UserRpcService userRpcService;
    @Resource(name = "taskExecutor")
    private ThreadPoolTaskExecutor threadPoolTaskExecutor;
    private static final Cache<Long, FindNoteDetailRspVO> LOCAL_CACHE = Caffeine.newBuilder()
            .initialCapacity(10000) // 设置初始容量为 10000 个条目
            .maximumSize(10000) // 设置缓存的最大容量为 10000 个条目
            .expireAfterWrite(1, TimeUnit.HOURS) // 设置缓存条目在写入后 1 小时过期
            .build();
    //编程事务
    @Resource
    private TransactionTemplate transactionTemplate;

    @Resource
    private RocketMQTemplate rocketMQTemplate;
    @Resource
    private NoteLikeMapper noteLikeMapper;

    @Override
    @Transactional(rollbackFor = Exception.class)
    public Response<?> publishNote(PublishNoteReqVO publishNoteReqVO) {
        Long userId = LoginUserContextHolder.getUserId();//发布者
        Integer type = publishNoteReqVO.getType();
        NoteTypeEnum noteTypeEnum = NoteTypeEnum.valueOf(type);
        // 若非图文、视频，抛出业务业务异常
        Preconditions.checkArgument(Objects.nonNull(noteTypeEnum), ResponseCodeEnum.NOTE_TYPE_ERROR.getErrorMessage());
        String imgUris = "";//图片地址
        String videosUrls = "";//视频地址

        // 根据类型进行图片、视频处理
        switch (noteTypeEnum) {
            case IMAGE_TEXT -> {
                log.info("发布图文笔记");
                List<String> imagesUrlList = publishNoteReqVO.getImgUris();
                // 校验图片是否为空
                Preconditions.checkArgument(CollUtil.isNotEmpty(imagesUrlList), "笔记图片不能为空");
                // 校验图片数量
                Preconditions.checkArgument(imagesUrlList.size() <= 8, "笔记图片不能多于 8 张");
                imgUris = StrUtil.join(",", imagesUrlList);
            }
            case VIDEO -> {
                String videoUri = publishNoteReqVO.getVideoUri();
                Preconditions.checkArgument(StrUtil.isNotBlank(videoUri), "笔记视频不能为空");
                videosUrls = videoUri;
            }
        }
        //处理话题----这是一个冗余字段
        String topicName = null;
        Long topicId = publishNoteReqVO.getTopicId();
        LambdaQueryWrapper<Topic> topicLqw = new LambdaQueryWrapper<>();
        topicLqw.eq(Topic::getId, topicId);
        Topic topic = topicMapper.selectOne(topicLqw);
        if (Objects.nonNull(topic)) {
            topicName = topic.getName();
        }


        //判断content是否为空吗，不为空则生成一个id
        String content = publishNoteReqVO.getContent();
        boolean isSuccessRedis = true;
        String contentUuid = "";
        if (StrUtil.isNotBlank(content)) {
            //生成uuid
            contentUuid = UUID.randomUUID().toString();
            isSuccessRedis = keyValueRpcService.saveNoteContent(contentUuid, content);
        }
        boolean isSuccessDB = true;
        Note note = Note.builder()
                .id(Long.valueOf(idGeneratorRpcService.getSnowflakeId()))
                .title(publishNoteReqVO.getTitle())
                .isContentEmpty(StrUtil.isBlank(content))
                .creatorId(userId)
                .topicId(topicId)
                .topicName(topicName)
                .isTop(false)
                .type(type)
                .imgUris(imgUris)
                .videoUri(videosUrls)
                .status(NoteStatusEnum.NORMAL.getCode())
                .contentUuid(contentUuid)
                .visible(NoteVisibleEnum.PUBLIC.getCode()).build();
        isSuccessDB = noteMapper.insert(note) > 0;
        if (!isSuccessDB || !isSuccessRedis) {
            keyValueRpcService.deleteNoteContent(contentUuid);
            throw new BizException(ResponseCodeEnum.NOTE_PUBLISH_FAIL);
        }
        return Response.success();
    }

    @Override
    public Response<FindNoteDetailRspVO> findNoteDetail(FindNoteDetailReqVO findNoteDetailReqVO) {
        Long noteId = findNoteDetailReqVO.getId();
        Long userId = LoginUserContextHolder.getUserId();
        //先查本地缓存
        FindNoteDetailRspVO localCacheNote = LOCAL_CACHE.getIfPresent(noteId);
        if (Objects.nonNull(localCacheNote)) {
            log.info("本地缓存笔记详情");
            checkNoteIsVisible(noteId, localCacheNote.getVisible(), userId);
            return Response.success(localCacheNote);
        }
        //再查redis
        String noteRedisKey = RedisKeyConstants.buildNoteDetailKey(noteId);
        String noteJson = (String) redisTemplate.opsForValue().get(noteRedisKey);
        if (StrUtil.isNotBlank(noteJson)) {
            //再次校验笔记是否可见
            FindNoteDetailRspVO findNoteDetailRspVO = JSONUtil.toBean(noteJson, FindNoteDetailRspVO.class);
            checkNoteIsVisible(noteId, findNoteDetailRspVO.getVisible(), userId);
            //异步构建本地缓存
            threadPoolTaskExecutor.execute(() -> {
                LOCAL_CACHE.put(noteId, findNoteDetailRspVO);
            });
            log.info("从redis缓存中获取笔记详情");
            return Response.success(findNoteDetailRspVO);
        }
        //判断缓存空值
        Preconditions.checkArgument(Objects.isNull(noteJson), ResponseCodeEnum.NOTE_NOT_FOUND.getErrorMessage());
        //查询db
        Note note = getById(noteId);
        //判断笔记是否存在
        if (Objects.isNull(note)) {
            //防止缓存穿透
            long expireSeconds = 60 + RandomUtil.randomInt(60);
            redisTemplate.opsForValue().set(noteRedisKey, "", expireSeconds, TimeUnit.SECONDS);
            throw new BizException(ResponseCodeEnum.NOTE_NOT_FOUND);
        }
        //判断是否可见
        Long creatorId = checkNoteIsVisible(note.getCreatorId(), note.getVisible(), userId);
        //获取用户信息
        String noteContent = null;
        FindUserByIdRspDTO findUserByIdRspDTO = userRpcService.findById(creatorId);
        Boolean isContentEmpty = note.getIsContentEmpty();

        if (!isContentEmpty) {

            noteContent = keyValueRpcService.findNoteContent(note.getContentUuid());
        }
        //处理笔记的信息
        List<String> imagesList = new ArrayList<>();
        String imgUris = note.getImgUris();
        if (StrUtil.isNotBlank(imgUris)) {
            imagesList = StrUtil.split(imgUris, ',');
        }
        FindNoteDetailRspVO findNoteDetailRspVO = BeanUtil.copyProperties(note, FindNoteDetailRspVO.class);
        findNoteDetailRspVO.setContent(noteContent);
        findNoteDetailRspVO.setAvatar(findUserByIdRspDTO.getAvatar());
        //异步构建redis缓存
        long expreireSeconds = 60 * 60 * 24 + RandomUtil.randomInt(60 * 60 * 24);//保底一天+随机秒数 防止缓存雪崩
        threadPoolTaskExecutor.execute(() -> {
            redisTemplate.opsForValue().set(noteRedisKey, JSONUtil.toJsonStr(findNoteDetailRspVO), expreireSeconds, TimeUnit.SECONDS);
        });
        return Response.success(findNoteDetailRspVO);
    }

    @Override
    public Response<?> updateNote(UpdateNoteReqVO updateNoteReqVO) {
        Long noteId = updateNoteReqVO.getId();
        Long userId = LoginUserContextHolder.getUserId();
        Note note = getById(noteId);
        Long creatorId = note.getCreatorId();
        //判断是否有权修改
        Preconditions.checkArgument(creatorId.equals(userId), "您没有权限修改该笔记");
        Integer type = updateNoteReqVO.getType();
        NoteTypeEnum noteTypeEnum = NoteTypeEnum.valueOf(type);
        Preconditions.checkArgument(NoteTypeEnum.isValid(type), "笔记类型错误");
        String imagUris = null;
        String videoUris = null;
        switch (noteTypeEnum) {
            case IMAGE_TEXT -> {
                List<String> imgUriList = updateNoteReqVO.getImgUris();
                Preconditions.checkArgument(CollUtil.isNotEmpty(imgUriList), "笔记图片不能为空");
                imagUris = StrUtil.join(",", imgUriList);
            }
            case VIDEO -> {
                String videoUrl = updateNoteReqVO.getVideoUri();
                Preconditions.checkArgument(StrUtil.isNotBlank(videoUrl), "笔记视频不能为空");
                videoUris = videoUrl;
            }
        }
        Long topicId = updateNoteReqVO.getTopicId();
        LambdaQueryWrapper<Topic> topicLqw = new LambdaQueryWrapper<>();
        topicLqw.eq(Topic::getId, topicId);
        Topic topic = topicMapper.selectOne(topicLqw);
        Preconditions.checkArgument(topic != null, "笔记话题不存在");
        // 笔记内容是否更新成功
        String noteRedisKey = RedisKeyConstants.buildNoteDetailKey(noteId);
        redisTemplate.delete(noteRedisKey);
        boolean isUpdateContentSuccess = false;
        String contentUuid = note.getContentUuid();//笔记内容 UUID
        String redisContent = RedisConstant.NOTE_CONTENT_KEY + contentUuid;
        String originContent = (String) redisTemplate.opsForValue().get(redisContent);
        String content = updateNoteReqVO.getContent();//传过来的笔记值
        if (StrUtil.isBlank(content)) {
            isUpdateContentSuccess = keyValueRpcService.deleteNoteContent(contentUuid);
            contentUuid = "";
        } else {
            contentUuid = StrUtil.isBlank(contentUuid) ? UUID.randomUUID().toString() : contentUuid;
            isUpdateContentSuccess = keyValueRpcService.saveNoteContent(contentUuid, content);
        }
        Note noteDO = Note.builder()
                .id(noteId)
                .isContentEmpty(StrUtil.isBlank(content))
                .imgUris(imagUris)
                .title(updateNoteReqVO.getTitle())
                .topicId(updateNoteReqVO.getTopicId())
                .topicName(topic.getName())
                .type(type)
                .updateTime(LocalDateTime.now())
                .videoUri(videoUris)
                .contentUuid(contentUuid)
                .build();
        boolean isUpdateDB = updateById(noteDO);
        if (!isUpdateDB && !isUpdateContentSuccess) {
            // 更新失败，恢复内容
            keyValueRpcService.saveNoteContent(note.getContentUuid(), originContent);
        }
        redisTemplate.delete(noteRedisKey);

        // 异步发送广播模式 MQ，将所有实例中的本地缓存都删除掉
        rocketMQTemplate.asyncSend(MQConstants.TOPIC_DELETE_NOTE_LOCAL_CACHE, noteId, new SendCallback() {
            @Override
            public void onSuccess(SendResult sendResult) {
                log.info("====> MQ：删除笔记本地缓存发送成功...");
            }

            @Override
            public void onException(Throwable throwable) {
                log.error("==> MQ：删除笔记本地缓存发送异常...", throwable);
            }
        });
        log.info("====> MQ：删除笔记本地缓存发送成功...");

        return Response.success();
    }


    /**
     * 检查笔记是否可见
     */
    private Long checkNoteIsVisible(Long creatorId, int visible, Long userId) {
        if (!Objects.equals(creatorId, userId) && !Objects.equals(visible, NoteVisibleEnum.PUBLIC.getCode())) {
            throw new BizException(ResponseCodeEnum.NOTE_PRIVATE);
        }
        return creatorId;
    }

    /**
     * 删除笔记本地缓存
     *
     * @param noteId
     */
    @Override
    public void deleteNoteLocalCache(Long noteId) {
        LOCAL_CACHE.invalidate(noteId);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public Response<?> deleteNote(DeleteNoteReqVO deleteNoteReqVO) {
        Long userId = LoginUserContextHolder.getUserId();
        Long noteId = deleteNoteReqVO.getId();
        Note note = getById(noteId);
        Preconditions.checkArgument(Objects.nonNull(note), ResponseCodeEnum.NOTE_NOT_FOUND.getErrorMessage());
        checkNoteIsVisible(note.getCreatorId(), note.getVisible(), userId);
        Long creatorId = note.getCreatorId();
        Preconditions.checkArgument(Objects.equals(userId, creatorId), ResponseCodeEnum.NOTE_NO_PERMISSION.getErrorMessage());
        boolean isDelete = removeById(noteId);
        String redisKey = RedisKeyConstants.buildNoteDetailKey(noteId);
        redisTemplate.delete(redisKey);
        boolean isSuccess = keyValueRpcService.deleteNoteContent(note.getContentUuid());

        if (!isDelete && !isSuccess) {
            //发生错误后回滚
            String noteContent = keyValueRpcService.findNoteContent(note.getContentUuid());
            keyValueRpcService.saveNoteContent(note.getContentUuid(), noteContent);
        }
        //异步删除本地缓存
        String destination = MQConstants.TOPIC_DELETE_NOTE_LOCAL_CACHE;
        rocketMQTemplate.asyncSend(destination, noteId, new SendCallback() {
            @Override
            public void onSuccess(SendResult sendResult) {
                log.info("====> MQ：删除笔记本地缓存发送成功...");
            }

            @Override
            public void onException(Throwable throwable) {
                log.error("==> MQ：删除笔记本地缓存发送异常...", throwable);
            }
        });
        return Response.success();
    }

    /**
     * 点赞接口
     * 首先使用rbitmap去进行判断用户是否有点赞笔记，如果说rbitmap这个集合不存在，那么就异步去进行同步，如果rbitmap中存在点赞记录那么直接抛出异常
     * 然后判断zset集合是否存在，如果说不存在，那么就进行同步--查数据库数据库存在的话抛一个异常
     * 都不存在的话就去创建一个笔记点赞记录
     * 如果
     *
     * @param likeNoteReqVO
     * @return
     */
    @Override
    public Response<?> likeNote(LikeNoteReqVO likeNoteReqVO) {
        Long noteId = likeNoteReqVO.getId();
        Long userId = LoginUserContextHolder.getUserId();
        //校验笔记是否存在
        Long creatorId = checkNoteIsExist(noteId);
        String bitmapKey = RedisKeyConstants.buildUserNoteLikeBitmapKey(userId);
        DefaultRedisScript<Long> bitmapCheckAndAddScript = new DefaultRedisScript<>();
        bitmapCheckAndAddScript.setResultType(Long.class);
        bitmapCheckAndAddScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/bitmap_note_like_check.lua")));
        Long checkAndAddRes = redisTemplate.execute(bitmapCheckAndAddScript, Collections.singletonList(bitmapKey), noteId);
        NoteLikeLuaResultEnum noteLikeLuaResultEnum = NoteLikeLuaResultEnum.valueOf(checkAndAddRes);
        String userNoteLikeKey = RedisKeyConstants.buildUserNoteLikeZSetKey(userId);
        switch (noteLikeLuaResultEnum) {
            case NOTE_LIKED -> throw new BizException(ResponseCodeEnum.NOTE_ALREADY_LIKED);
            case NOT_EXIST -> {
                threadPoolTaskExecutor.submit(() -> {
                    syncNoteLikeBitmap();
                });
                //查redis
                Double score = redisTemplate.opsForZSet().score(userNoteLikeKey, noteId);
                if (Objects.isNull(score)) {
                    //查数据库
                    LambdaQueryWrapper<NoteLike> lqw = new LambdaQueryWrapper<>();
                    lqw.eq(NoteLike::getNoteId, noteId);
                    lqw.eq(NoteLike::getUserId, userId);
                    lqw.eq(NoteLike::getStatus, NoteStatusEnum.NORMAL.getCode());
                    Long count = noteLikeMapper.selectCount(lqw);
                    if (count != 0) {
                        //异步同步redis和bitmap
                        threadPoolTaskExecutor.submit(() -> {
                            syncNoteLikeRedis();
                            syncNoteLikeBitmap();
                        });
                        throw new BizException(ResponseCodeEnum.NOTE_ALREADY_LIKED);
                    }
                    //添加点赞记录
                    long expireSeconds = 60 * 60 * 24 + RandomUtil.randomInt(60 * 60 * 24);
                    DefaultRedisScript<Long> rbitmapAddScript = new DefaultRedisScript<>();
                    rbitmapAddScript.setResultType(Long.class);
                    rbitmapAddScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/rbitmap_note_like_add_and_expire.lua")));
                    redisTemplate.execute(rbitmapAddScript, Collections.singletonList(bitmapKey), noteId, expireSeconds);
                } else {
                    throw new BizException(ResponseCodeEnum.NOTE_ALREADY_LIKED);
                }
            }
        }
        //操作redis
        syncNoteLikeRedis();
        Boolean hasKey = redisTemplate.hasKey(userNoteLikeKey);
        if (hasKey) {
            long timestamp = DateUtils.localDateTime2Timestamp(LocalDateTime.now());
            redisTemplate.opsForZSet().add(userNoteLikeKey, noteId, timestamp);
        } else {
            DefaultRedisScript<Long> zsetAddAndExpireScript = new DefaultRedisScript<>();
            zsetAddAndExpireScript.setResultType(Long.class);
            zsetAddAndExpireScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/zset_note_like_add_and_expire.lua")));
            long timestamp = DateUtils.localDateTime2Timestamp(LocalDateTime.now());
            long expireSeconds = 60 * 60 * 24 + RandomUtil.randomInt(60 * 60 * 24);
            redisTemplate.execute(zsetAddAndExpireScript, Collections.singletonList(userNoteLikeKey), noteId, timestamp, expireSeconds);
        }
        //mq落库
        LikeUnlikeNoteMqDTO likeUnlikeNoteMqDTO = LikeUnlikeNoteMqDTO.builder()
                .userId(userId)
                .noteId(noteId)
                .creatorId(creatorId)
                .type(LikeUnlikeNoteTypeEnum.LIKE.getCode()) // 点赞笔记
                .createTime(LocalDateTime.now())
                .build();
        String hashKey = userId.toString();
        Message<String> message = MessageBuilder.withPayload(JSONUtil.toJsonStr(likeUnlikeNoteMqDTO)).build();
        String destination = MQConstants.TOPIC_LIKE_OR_UNLIKE + ":" + MQConstants.TAG_LIKE;
        rocketMQTemplate.asyncSendOrderly(destination, message, hashKey, new SendCallback() {
            @Override
            public void onSuccess(SendResult sendResult) {
                log.info("==> 点赞笔记消息发送成功，SendResult: {}", sendResult);
            }

            @Override
            public void onException(Throwable throwable) {
                log.error("==> 点赞笔记消息发送异常: ", throwable);
            }
        });
        return Response.success();
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public Response<?> unlikeNote(UnlikeNoteReqVO unlikeNoteReqVO) {
        Long noteId = unlikeNoteReqVO.getId();
        Long userId = LoginUserContextHolder.getUserId();
        Long creatorId = checkNoteIsExist(noteId);
        String bitmapKey = RedisKeyConstants.buildUserNoteLikeBitmapKey(userId);
        String redisKey = RedisKeyConstants.buildUserNoteLikeZSetKey(userId);
        DefaultRedisScript<Long> rbitmapCheckAndDelScript = new DefaultRedisScript<>();
        rbitmapCheckAndDelScript.setResultType(Long.class);
        rbitmapCheckAndDelScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/bitmap_note_unlike_check.lua")));
        Long checkAndDelRes = redisTemplate.execute(rbitmapCheckAndDelScript, Collections.singletonList(bitmapKey), noteId);
        NoteUnlikeLuaResultEnum noteUnlikeLuaResultEnum = NoteUnlikeLuaResultEnum.valueOf(checkAndDelRes);
        switch (noteUnlikeLuaResultEnum) {
            case NOT_LIKED -> throw new BizException(ResponseCodeEnum.NOTE_NOT_LIKED);
            case NOT_EXIST -> {
                //查redis
                Double score = redisTemplate.opsForZSet().score(redisKey, noteId);
                if (Objects.isNull(score)) {
                    //查数据库
                    LambdaQueryWrapper<NoteLike> lqw = new LambdaQueryWrapper<>();
                    lqw.eq(NoteLike::getNoteId, noteId);
                    lqw.eq(NoteLike::getUserId, userId);
                    lqw.eq(NoteLike::getStatus, NoteStatusEnum.NORMAL.getCode());
                    Long count = noteLikeMapper.selectCount(lqw);
                    if (count == 0) {
                        throw new BizException(ResponseCodeEnum.NOTE_NOT_LIKED);
                    } else {
                        //异步同步redis和bitmap
                        threadPoolTaskExecutor.submit(() -> {
                            syncNoteLikeRedis();
                            syncNoteLikeBitmap();
                        });
                    }
                }
            }
        }
        //修改redis数据
        if (redisTemplate.hasKey(redisKey)) {
            redisTemplate.opsForZSet().remove(redisKey, noteId);
        } else {
            syncNoteLikeRedis();
            redisTemplate.opsForZSet().remove(redisKey, noteId);
        }
        //mq落库
        LikeUnlikeNoteMqDTO likeUnlikeNoteMqDTO = LikeUnlikeNoteMqDTO
                .builder()
                .userId(userId)
                .noteId(noteId)
                .creatorId(creatorId)
                .type(LikeUnlikeNoteTypeEnum.UNLIKE.getCode()) // 取消点赞笔记
                .createTime(LocalDateTime.now())
                .build();
        String hashKey = userId.toString();
        Message<String> message = MessageBuilder.withPayload(JSONUtil.toJsonStr(likeUnlikeNoteMqDTO)).build();
        String destination = MQConstants.TOPIC_LIKE_OR_UNLIKE + ":" + MQConstants.TAG_UNLIKE;
        rocketMQTemplate.asyncSendOrderly(destination, message, hashKey, new SendCallback() {
            @Override
            public void onSuccess(SendResult sendResult) {
                log.info("==> 取消点赞笔记消息发送成功，SendResult: {}", sendResult);
            }

            @Override
            public void onException(Throwable throwable) {
                log.error("==> 取消点赞笔记消息发送异常: ", throwable);
            }
        });
        return Response.success();
    }

    /**
     * 校验笔记是否存在
     *
     * @param noteId
     * @return
     */
    private Long checkNoteIsExist(Long noteId) {
        FindNoteDetailRspVO localNote = LOCAL_CACHE.getIfPresent(noteId);
        Long creatorId = null;
        if (Objects.isNull(localNote)) {
            String redisNoteKey = RedisKeyConstants.buildNoteDetailKey(noteId);
            String redisJson = (String) redisTemplate.opsForValue().get(redisNoteKey);
            if (StrUtil.isBlank(redisJson)) {
                Note note = getById(noteId);
                if (Objects.isNull(note)) {
                    throw new BizException(ResponseCodeEnum.NOTE_NOT_FOUND);
                }
                FindNoteDetailReqVO findNoteDetailReqVO = FindNoteDetailReqVO.builder()
                        .id(noteId)
                        .build();
                threadPoolTaskExecutor.submit(() -> {
                    findNoteDetail(findNoteDetailReqVO);
                });
                return note.getCreatorId();
            }
            localNote = JSONUtil.toBean(redisJson, FindNoteDetailRspVO.class);
            return localNote.getCreatorId();
        }
        creatorId = localNote.getCreatorId();
        return creatorId;
    }

    /**
     * 构建 Lua 脚本参数：点赞列表
     */
    private static Object[] buildLikeZSetLuaArgs(List<NoteLike> noteLikeList, long expireSeconds) {
        int argsLength = noteLikeList.size() * 2 + 1; // 每个粉丝关系有 2 个参数（score 和 value），再加一个过期时间
        Object[] luaArgs = new Object[argsLength];

        int i = 0;
        for (NoteLike noteLike : noteLikeList) {
            luaArgs[i] = DateUtils.localDateTime2Timestamp(noteLike.getCreateTime()); // 粉丝的关注时间作为 score
            luaArgs[i + 1] = noteLike.getNoteId();          // 粉丝的用户 ID 作为 ZSet value
            i += 2;
        }

        luaArgs[argsLength - 1] = expireSeconds; // 最后一个参数是 ZSet 的过期时间
        return luaArgs;
    }

    /**
     * 同步redis
     */
    private void syncNoteLikeRedis() {
        LambdaQueryWrapper<NoteLike> noteLikeLqw = new LambdaQueryWrapper<>();
        Long userId = LoginUserContextHolder.getUserId();
        noteLikeLqw.eq(NoteLike::getUserId, userId);
        noteLikeLqw.eq(NoteLike::getStatus, NoteStatusEnum.NORMAL.getCode());
        List<NoteLike> noteLikes = noteLikeMapper.selectList(noteLikeLqw);
        if (noteLikes.size() > 0 && CollUtil.isNotEmpty(noteLikes)) {
            String userNoteLikeKey = RedisKeyConstants.buildUserNoteLikeZSetKey(userId);
            long expireSeconds = 60 * 60 * 24 + RandomUtil.randomInt(60 * 60 * 24);
            Object[] luaArgs = buildLikeZSetLuaArgs(noteLikes, expireSeconds);
            DefaultRedisScript<Long> script = new DefaultRedisScript<>();
            script.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/like_batch_add_and_expire.lua")));
            script.setResultType(Long.class);
            redisTemplate.execute(script, Collections.singletonList(userNoteLikeKey), luaArgs);
        }
    }

    /**
     * 同步bitmap
     */
    private void syncNoteLikeBitmap() {
        log.info("==> 同步bitmap");
        Long userId = LoginUserContextHolder.getUserId();
        LambdaQueryWrapper<NoteLike> noteLikeLqw = new LambdaQueryWrapper<>();
        noteLikeLqw.eq(NoteLike::getUserId, userId);
        noteLikeLqw.eq(NoteLike::getStatus, NoteStatusEnum.NORMAL.getCode());
        List<NoteLike> noteLikes = noteLikeMapper.selectList(noteLikeLqw);
        if (noteLikes.size() > 0 && CollUtil.isNotEmpty(noteLikes)) {
            String bitmapKey = RedisKeyConstants.buildUserNoteLikeBitmapKey(userId);
            DefaultRedisScript<Long> script = new DefaultRedisScript<>();
            script.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/rbitmap_batch_add_note_like_and_expire.lua")));
            script.setResultType(Long.class);
            // 构建 Lua 参数
            List<Object> luaArgs = Lists.newArrayList();
            noteLikes.forEach(noteLike -> luaArgs.add(noteLike.getNoteId()));
            long expireSeconds = 60 * 60 * 24 + RandomUtil.randomInt(60 * 60 * 24);
            luaArgs.add(expireSeconds);
            redisTemplate.execute(script, Collections.singletonList(bitmapKey), luaArgs.toArray());
        }
    }

}




